iT邦幫忙

2024 iThome 鐵人賽

DAY 14
0
JavaScript

Don't make JavaScript Just Surpise系列 第 14

運算子(Operator) 下篇(含JS 中的運算子優先級/序)

  • 分享至 

  • xImage
  •  

這篇接續討論上篇沒討論到的類別。

  • 算術運算子
  • 字串運算子
  • 位元運算子
  • 條件運算子(又稱三元運算子)
  • 逗點運算子
  • 一元運算子
  • 關係運算子

算術運算子和字串運算子

算術運算子除了基本的加減乘除外,還有 %++---(負號),+(正號),**
字串運算子只有一個 +

有經驗的開發者對這些運算子的個別用途應該都理解了,我就不對個別作用做說明,而是提一下使用時需要注意的地方。

自增和自減運算子的順序(++--)

自增和自減運算子有個比較特別的東西,在其他語言也會看到:他們都是一元運算子,同時,他可以被放在運算元的前面或後面。(前置或後置,指的是運算子放的位置,++a 稱作前置)

let a = 0;
console.log(a++);//0
console.log(++a);//2
let b = 0;
console.log(b--);//0
console.log(--b);//-2

在無需立刻取值的情況下,前置後置並無差別。
放置位置會決定他的執行順序。
但需要取值的時候,前置會先運算後取值,後置會先取值後運算。
a++ 印出 0 因為先取值;--b 印出 -2 因為先運算。
自增自減運算子還是會做隱式轉換,所以如果丟字串會拿到嘗試轉數字的結果,無法成功轉則拿到 NaN

console.log("1"++);

那這個例子呢?答案是會拿到錯誤。
為什麼?因為 ++-- 他們只能對可變數值做操作,"1"本身是個字串,是個不可變類別(原始型別皆是),所以會拋出錯誤:

Uncaught SyntaxError: Invalid left-hand side expression in postfix operation"

那麼這樣呢?

console.log(undefined++);//NaN
console.log(null++);//SyntaxError: Invalid left-hand side expression in postfix operation

跟你預期的一樣嗎?++ 做了轉數字的隱式型別轉換,根據規範 undefined 轉為 NaN,而 null 轉為 0
0 是不可變的原始型別 number,而對 NaN 進行操作只會拿到 NaN,這就是為什麼這兩行的結果是這樣的。

console.log(Number(undefined));//NaN
console.log(Number(null));//0

字串相加與規則

+ 同時能對數字和文字生效應該雷過不少人。
具體的規則是,從左往右看(在沒有括號的情況下,不考慮運算子優先級),第一個加號的兩邊其中一個運算元是字串,則從此刻開始所有該運算式中的加號做為字串運算子使用。

console.log(5 + 5 + "5");//"105",前面兩個正常加,第二次變字串相加
console.log(5 + "5" + 5);//"555",第一個就開始是字串相加
console.log("5" + 5 + 5);//"555",第一個就開始是字串相加,在左在右沒有差別,都是屬於第一個加的運算元

console.log("5" + 5 * 5);//"525",當有運算子優先級參進來,則高優先級的先處理完,5*5 = 25,再把 5 和 25 做字串相加

所以當要明標該串連加為字串相加,其中一個技巧是在開頭使用""作為第一個加號的運算元,則整串相加式在該等級的優先度都會被認為是字串相加(如果後面又有括號改變優先級,則括號內依然會先自己處理完)。

console.log("" + 5 + 5 + 5);//"555"
console.log("" + 5 + (5 + 5));//"510",括號內有高優先級,故先處理

加號優先處理字串相加的情況,只有當兩邊皆為數字型別時才會進行數字運算的相加(數字型別包含 bigint)。
所以如果是其他的型別,如物件,也會被先轉為字串再相加。

console.log({} + {});//"[object Object][object Object]"
console.log(123n+123n);//246n

除加號外,其他算數運算子都會嘗試將運算元轉為數字型別,如:

console.log("1"-0, typeof ("1"-0));//1 number

透過減法做一次 -0,運算結果就變為了數字。

位元運算子

位元運算子包含 &|^~<<>>>>>
位元運算子會把運算元當轉為數值後以 32 位元的集合來看(0 或 1),但回傳時會轉為 JS 中一般十進位的方式顯示。

let a = "1";
let b = 1;
let c = "a";
console.log(a << a);//2
console.log(b << b);//2
console.log(c << 1);//0
console.log(c << 1 | 1);//1

第三個式子為什麼是 0? 因為位元運算一樣會做隱式轉換把參與的運算元轉為數字型別,轉不了的時候一樣是 NaN,但位元運算會NaN 即刻當作 0 來處理
所以第四個式子即使先前是 NaN,做 | 運算時一樣能拿到 1 的結果。

另外位元運算子的上限比數字來的低,number 型別是 64 位元的 IEEE 754 雙精度浮點數,而正如上面所說,位元運算子會轉為 32 位元來進行運算,超過 32 位元的都會發出溢位的狀況。
最大值為 2^32 - 1(2,147,483,648),最小值為 -2^31(-2,147,483,648)。
做大數運算的時候要特別小心這點。

~ 運算子的妙用

~ 運算子做的事情是把,每個位元反轉。

console.log(~1);//-2
console.log(~0);//-1
console.log(~-1);//0

有一個比較特別的用法是用於條件判斷。
因為很多找尋式的找不到會回傳 -1,如 indexOf()。
加上 ~ 會把 -1 轉為 0,而 0 在條件判斷中恰好是個 falsy 的值。

let arr = [1,2,3];
if(~arr.indexOf(4)){console.log("Found");}
else{console.log("Not Found");}

如果沒有 ~ 回傳的 -1truthy,會進到 "Found"
但如果有 ~ 就能正確顯示 "Not Found",是一種相較於 arr.indexOf(4) != -1 更優雅的寫法。(當然需要團隊是能夠理解這樣寫法的意圖的情況)

三元運算子 / 條件運算子

JS 中僅有一個三元運算子,就是 condition ? exprIfTrue : exprIfFalse,可以看作是 if...else 的簡寫版。在絕大多數的語言中,三元運算子通常也是作為條件運算子使用。
如果 if 的狀況,只會根據 condition 檢查對應的結果並返回,依然有類似邏輯短路的效果。

let a = {foo:'123'};
console.log(true?a.foo.length:a.bar.length);//3,並不會有錯誤
console.log(false?a.foo.length:a.bar.length);//拋錯

三元是個簡潔的寫法,唯有要避免過度嵌套,或過長的敘述句,就像 if 也往往不會進行過多層寫法(義大利麵程式碼)。

逗號運算子

逗號運算子只有一個 ,,就像名字一樣。
允許在一個運算式中,運算多個子運算式,並返回最後一個子運算式的結果。

let x = (1,2,3);
console.log(x);//3

常見於 for 迴圈中,用做多個遞增遞減運算式。

let j = 0;
for(let i = 0; i < 5; i++, j++){
    console.log(i);
    console.log(j);
}

大多數我們只用在這種情況,避免影響可讀性(返回最後一個子運算式的特性),其他狀況我個人傾向於使用多行來表達。

一元運算子

這邊是按 MDN 的分類來的,其實前面我們已經看過很多一元運算子了,包含 ++--,或正負號等等。
typeofdeletevoid 這些都算是一元運算子
這些一元運算字看起來像是函式,但其實算做運算子。
特別區分出來是想說運算子不一定只有符號,這些被定義的關鍵字也有可能是運算子。
和函式的不同在於 1. 只能接收一個運算元(一元) 2.無需括號 3.全小寫(對運算子的語言規範,不然以小駝峰 typeof 應該寫成 typeOf)。

關係運算子

關係運算子指的是 ininstanceof
in 用於檢查某個屬性是否存於對象物件中。

let a = {foo:'123'};
console.log('foo' in a);//true
console.log('bar' in a);//false

檢查時需傳入一個能被轉為字串的值作為查詢的屬性名稱,依結果回傳 true / false
in 在檢查的時候包含其原型鏈上的屬性,且不會特別去管屬性的值是什麼,只處理屬性是否存在。

let a = function(){this.foo = '123'};
let b = new a();
console.log('foo' in b);//true

instanceof 則是用於檢查兩個物件,是否其中一個為另一個的建構對象。

let a = function(){this.foo = '123'};
let b = new a();
console.log(b instanceof a);//true
console.log(b instanceof Object);//true
console.log(b instanceof object);//Uncaught TypeError: Right-hand side of 'instanceof' is not callable"

console.log(b.__proto__ == a.prototype);//true

同樣是經由原型鏈做判斷,如果該物件的建構式存在於原型鏈上,則回傳 true,否則回傳 false

運算子優先級/運算子優先序(Operator precedence)

上面把運算子大致介紹告一段落。
來提提運算子優先級吧,在實際世界裡,運算子多為混合使用的,那當混合使用的時候,誰先處理,誰後處理?
最知名的莫過於口訣:先乘除後加減。

console.log( 5 + 5 * 5);//30,因為先 5 * 5

這樣的式子就表明了,作為算術運算子的 * 的優先度是高於 + 的。

MDN 上有個非常充足的表格,請點進去參閱。

裡面寫的相依性和我前文說的結合性是一樣的,同指 Associativity
結合性負責處理優先序相同的時候運算式的執行方向。
從上面連結的表格可以看到,沒有被歸類在相同優先序但結合性不同的例子(同一個優先序內,總是左結合或右結合,或無結合性)。

表格分得很細,我們抽高一點來看大致的排序,以下為高到低:

  1. 括號 (),括號內必定為最優先,從內部的括號開始往外處理
  2. 存取 . [],使用或存取優先度僅次於括號,在其他任何運算之前
  3. 所有一元運算子們 ++ -- typeof !(包含像 ! 這種本來被歸類於邏輯運算子中的)
  4. 四則運算們,其中 ** 優先序最高,再來是 *-/,最後是 +-
  5. 位元運算子的一部份 >> << >>>
  6. 比較運算子們 == >= 等等和關係運算子們 in instanceof
  7. 剩下的位元運算子們,有優先序差 & > ^ > |
  8. 邏輯運算子們,有優先序差 && > || > ??
  9. 三元運算子 ? :,所以三元運算常常需要搭配 () 使用確保表達式,三元返回值都是清楚的,否則可能會被高優先序的運算子影響結果。
  10. 賦值運算子,如 = += 等等
  11. 逗號運算子,優先度最低

其實優先序大致上可以用邏輯來思考,() 第一是沒有爭議的,再來我們需要先取值才能做計算。
做計算的時候必須先處理一元計算子,看看被一元計算子影像後的計算元變成了什麼樣新的值。
接著開始做二元計算,二元計算中四則運算大於位元計算,至此算出了大部分的值,所以我們可以開始做邏輯運算比較值的結果,邏輯比較完可以賦值(返回值)。
最後是逗號運算子可以表示多個子運算式。

比較要特別記得大概是 4 ~ 6 這段,直接對位元做位元數操作優於比較,兩個樹之間做位元操作低於比較。
另外優先序上三元幾乎是最低的,要記得加入對應的括號來適當提升對應式子的優先度,特別是在嵌套的情況下。也可以注意到優先序上,一元高於二元,二元高於三元。


至此將所有的運算子及其相關重要知識大概都已提及。


上一篇
運算子(Operator) 上篇
下一篇
事件循環(Event Loop)
系列文
Don't make JavaScript Just Surpise31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言